/**
 * 版权所有(C)，上海海鼎信息工程股份有限公司，2007，所有权利保留。
 * 
 * 项目名：	rumba
 * 文件名：	MonthlySchedule.java
 * 模块说明：	
 * 修改历史：
 * Jun 19, 2007 - lxm - 创建。
 */
package com.hd123.rumba.runtime.backprocess;

import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;

import com.hd123.rumba.exception.BusinessException;
import com.hd123.rumba.util.DateUtil;

/**
 * 每月后台任务调度 | 实现类
 * <p>
 * 
 * 调度公式1：从{@link #startDate}到{@link #finishDate}期间，每间隔{@link #every}月的第{@link #numberOfDay}天的指定时间{@link #time}被触发。<br>
 * 调度公式2：从startDate到finishDate期间，每间隔every月的第{@link #numberOfDOW}个{@link #dayOfWeek}的指定时间{@link #time}被触发。
 * <p>
 * 
 * 关于周期的计算，把startDate所在月作为第一月，将其后的时间按照every指定的月数分割为若干段，每个段的第一月中指定日期的指定时间被触发。
 * 
 * @author lxm
 * 
 */
public class MonthlySchedule implements ITaskSchedule {

  private Date time;
  private int every = 1;
  private Date startDate;
  private Date finishDate = null;
  private Style style = Style.BYDAY;
  private int numberOfDay = 1;
  private int numberOfDOW = 1;
  private int dayOfWeek = Calendar.SUNDAY;

  /** 风格取值。 */
  public enum Style {
    /** 采用调度公式1 */
    BYDAY("byDay"),
    /** 采用调度公式2 */
    BYWEEK("byWeek");

    private String value;

    private Style(String value) {
      this.value = value;
    }
    
    public static Style getStyle(String value) {
      if (BYDAY.getValue().equals(value))
        return BYDAY;
      if (BYWEEK.getValue().equals(value))
        return BYWEEK;
      return null;
    }

    public String getValue() {
      return this.value;
    }
  }

  public MonthlySchedule() {
    super();
    this.time = DateUtil.getTimePart(new Date());
    this.startDate = DateUtil.truncate(new Date(), Calendar.DATE);
  }

  /**
   * 属性：指定周的第几天。可选值为：{@link Calendar#SUNDAY}, ..., {@link Calendar#SATURDAY}<br>
   * 仅当style==Style.BYWEEK时有效。
   */
  public int getDayOfWeek() {
    return dayOfWeek;
  }

  public void setDayOfWeek(int dayOfWeek) throws BusinessException {
    assert (dayOfWeek >= Calendar.SUNDAY && dayOfWeek <= Calendar.SATURDAY);
    if (dayOfWeek < Calendar.SUNDAY || dayOfWeek > Calendar.SATURDAY)
      throw new BusinessException("未知一周中的日子\"" + dayOfWeek + "\"。");

    this.dayOfWeek = dayOfWeek;
  }

  /**
   * 属性：指定第几天。不允许0；允许<0，表示一个月的最后第几天。<br>
   * 仅当style==Style.BYDAY时有效。
   */
  public int getNumberOfDay() {
    return numberOfDay;
  }

  public void setNumberOfDay(int numberOfDay) throws BusinessException {
    assert (numberOfDay != 0);
    if (numberOfDay == 0)
      throw new BusinessException("属性numberOfDay必须是非0的整数。");

    this.numberOfDay = numberOfDay;
  }

  /**
   * 属性：指定第几个{@link #dayOfWeek}。不允许0；允许<0，表示一个月的倒数第几个。<br>
   * 仅当style==Style.BYWEEK时有效。
   */
  public int getNumberOfDOW() {
    return numberOfDOW;
  }

  public void setNumberOfDOW(int numberOfDOW) throws BusinessException {
    assert (numberOfDOW != 0);
    if (numberOfDOW == 0)
      throw new BusinessException("属性numberOfDOW必须是非0的整数。");
    this.numberOfDOW = numberOfDOW;
  }

  /** 属性：计算方式。 */
  public Style getStyle() {
    return style;
  }

  public void setStyle(Style style) throws BusinessException {
    if (style == null)
      throw new BusinessException("属性style不允许为null。");
    this.style = style;
  }

  /** 属性：在一天中的时间。将忽略日期部分。 */
  public Date getTime() {
    return time;
  }

  public void setTime(Date time) throws BusinessException {
    assert time != null;
    if (time == null)
      throw new BusinessException("属性time不允许为null。");
    this.time = DateUtil.getTimePart(time);
  }

  /**
   * 设置在一天中的时间。
   * 
   * @param hour
   *          指定小时部分，24小时制。
   * @param minute
   *          指定分钟部分。
   * @param second
   *          指定秒部分。
   */
  public void setTime(int hour, int minute, int second) {
    Calendar c = Calendar.getInstance();
    c.clear();
    c.set(Calendar.HOUR_OF_DAY, hour);
    c.set(Calendar.MINUTE, minute);
    c.set(Calendar.SECOND, second);
    this.time = c.getTime();
  }

  /** 间隔天数。默认为1，&gt;0。 */
  public int getEvery() {
    return every;
  }

  /** 间隔天数。默认为1，&gt;0。 */
  public void setEvery(int every) throws BusinessException {
    if (every <= 0)
      throw new BusinessException("属性every必须为大于等于1的整数。");
    this.every = every;
  }

  /** 结束日期。包括这一天。允许null，表示不指定结束日期。 */
  public Date getFinishDate() {
    return finishDate;
  }

  /** 结束日期。包括这一天。允许null，表示不指定结束日期。 */
  public void setFinishDate(Date finishDate) {
    this.finishDate = DateUtil.truncate(finishDate, Calendar.DATE);
  }

  /** 开始日期 */
  public Date getStartDate() {
    return startDate;
  }

  /** 开始日期 */
  public void setStartDate(Date startDate) {
    this.startDate = DateUtil.truncate(startDate, Calendar.DATE);
  }

  private static final String[] CAPTION_DAYOFWEEK = {
      "星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六" };
  private static final SimpleDateFormat sdfTime = new SimpleDateFormat("HH:mm:ss");
  private static final SimpleDateFormat sdfDate = new SimpleDateFormat("yyyy-MM-dd");

  public String getDescription() throws BusinessException {
    StringBuffer sb = new StringBuffer();
    sb.append("从");
    sb.append(sdfDate.format(this.startDate));
    sb.append("开始");
    if (this.finishDate != null) {
      sb.append("到");
      sb.append(sdfDate.format(this.finishDate));
      sb.append("期间");
    }
    if (this.every == 1)
      sb.append("每个月");
    else {
      sb.append("每");
      sb.append(this.every);
      sb.append("个月");
    }
    sb.append("的");
    if (Style.BYDAY.equals(this.style)) {
      if (this.numberOfDay < 0)
        sb.append("倒数");
      sb.append("第");
      sb.append(Math.abs(this.numberOfDay));
      sb.append("天的");
    } else {
      if (this.numberOfDOW < 0)
        sb.append("倒数");
      sb.append("第");
      sb.append(Math.abs(this.numberOfDOW));
      sb.append("个");
      sb.append(CAPTION_DAYOFWEEK[this.dayOfWeek]);
      sb.append("的");
    }
    sb.append(sdfTime.format(this.time));
    sb.append("执行。");

    return sb.toString();
  }

  public Date getNextTime() throws BusinessException {
    return getNextTime(new Date());
  }

  private static final int MONTHS_OF_YEAR = 12;

  public Date getNextTime(Date baseTime) throws BusinessException {
    if (baseTime == null)
      baseTime = new Date();

    // 找到baseTime所在周期时间段（根据every进行分段）的第一天，作为后面计算的一个基准日期。
    Date startFirstDate = DateUtil.truncate(this.startDate, Calendar.MONTH);
    int nStartMonths = DateUtil.get(startFirstDate, Calendar.YEAR) * MONTHS_OF_YEAR
        + DateUtil.get(startFirstDate, Calendar.MONTH);
    int nBaseMonths = DateUtil.get(baseTime, Calendar.YEAR) * MONTHS_OF_YEAR
        + DateUtil.get(baseTime, Calendar.MONTH);
    int nSecs = 0;
    if (nBaseMonths >= nStartMonths)
      nSecs = (int) Math.floor((nBaseMonths - nStartMonths)
          / (double) (this.every * MONTHS_OF_YEAR));
    Date firstDate = DateUtil.add(startFirstDate, Calendar.MONTH, nSecs * this.every);

    Date nextTime = null;
    while (nextTime == null) {
      // 找到基准日所在月中有效的那一天，
      Date date = null;
      if (Style.BYDAY.equals(this.style))
        date = getValidDateByDay(firstDate);
      else
        date = getValidDateByWeek(firstDate);
      Date datetime = DateUtil.addDateAndTime(date, this.time);
      if (datetime.after(this.startDate) && datetime.after(baseTime)) {
        nextTime = datetime;
        break;
      }
      // 调整基准日到下一个周期时间段的第一天。
      firstDate = DateUtil.add(firstDate, Calendar.MONTH, this.every);
    }

    // 结束日期检查
    if (this.finishDate != null && !nextTime.before(DateUtil.add(this.finishDate, Calendar.DATE, 1)))
      nextTime = null;
    return nextTime;
  }

  /**
   * 判断指定的时间是否是某个触发时间。
   * 
   * @param aTime
   *          指定的时间。
   * @return
   */
  public boolean isValid(Date aTime) {
    if (aTime == null)
      return false;

    // 检查是否在开始与结束日期之间
    if (aTime.before(this.startDate))
      return false;
    if (this.finishDate != null && !aTime.before(DateUtil.add(this.finishDate, Calendar.DATE, 1)))
      return false;

    // 检查时间部分
    if (DateUtil.getTimePart(aTime).compareTo(this.time) != 0)
      return false;
    
    // 检查是否是分段的第一周。
    Date startFirstDate = DateUtil.truncate(this.startDate, Calendar.MONTH);
    int nStartMonths = DateUtil.get(startFirstDate, Calendar.YEAR) * MONTHS_OF_YEAR
        + DateUtil.get(startFirstDate, Calendar.MONTH);
    int nAMonths = DateUtil.get(aTime, Calendar.YEAR) * MONTHS_OF_YEAR
        + DateUtil.get(aTime, Calendar.MONTH);
    double secs = (nAMonths - nStartMonths) / (double) this.every;
    if (Math.floor(secs) != secs)
      return false;
    
    // 检查是否在当前月内是有效日期
    Date aDate = DateUtil.truncate(aTime, Calendar.DATE);
    Date firstDate = DateUtil.truncate(aTime, Calendar.MONTH);
    Date validDate = null;
    if (Style.BYDAY.equals(this.style))
      validDate = this.getValidDateByDay(firstDate);
    else
      validDate = this.getValidDateByWeek(firstDate);
    return aDate.compareTo(validDate) == 0;
  }

  /** 在指定日期所在月寻找有效的那一天（style == BYDAY）。 */
  private Date getValidDateByDay(Date firstDate) {
    assert DateUtil.truncate(firstDate, Calendar.MONTH).compareTo(firstDate) == 0;

    if (this.numberOfDay > 0)
      return DateUtil.add(firstDate, Calendar.DAY_OF_MONTH, this.numberOfDay - 1);
    else {
      Date date = DateUtil.add(firstDate, Calendar.MONTH, 1);
      return DateUtil.add(date, Calendar.DATE, this.numberOfDay);
    }
  }

  /** 在指定日期所在月寻找有效的那一天（style == BYWEEK）。 */
  private Date getValidDateByWeek(Date firstDate) {
    assert DateUtil.truncate(firstDate, Calendar.MONTH).compareTo(firstDate) == 0;

    Calendar cDate = Calendar.getInstance();
    if (this.numberOfDOW > 0)
      cDate.setTime(firstDate);
    else {
      Date date = DateUtil.add(firstDate, Calendar.MONTH, 1);
      date = DateUtil.add(date, Calendar.DATE, -1);
      cDate.setTime(date);
    }

    Date validDate = null;
    int cnt = 0;
    do {
      if (cDate.get(Calendar.DAY_OF_WEEK) == this.dayOfWeek) {
        cnt++;
        if (cnt == Math.abs(this.numberOfDOW)) {
          validDate = cDate.getTime();  
          break;
        }
      }
      cDate.add(Calendar.DATE, (int) Math.signum(this.numberOfDOW));
    } while (cDate.get(Calendar.MONTH) == DateUtil.get(firstDate, Calendar.MONTH));
    
    return validDate;
  }
}
